﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Input;
using uam_fps_game.Settings;

namespace uam_fps_game.Cameras
{
    // Prosta kamera FPP
    public class FpsCamera
    {
        // Obowiazkowe macierze widoku i projekcji. Trzy ponizsze pola to tak zwane Wlasciwosci (Properties) obiektu.
        // Polecam poczytac o Properties w jezyku C# (jest to mechanizm charakterystyczny dla tego jezyka).
        public Matrix View { get; protected set; }
        public Matrix Projection { get; protected set; }

        // Parametry uzywane do tworzenia macierzy View. Czesc z nich jest bezposrednio modyfikowana w wyniku wczytania inputu od gracza,
        // czesc jest zalezna od innych, patrz kod i komentarze ponizej.
        public Vector3 Position { get; protected set; }
        private Vector3 target;

        private Vector3 up;

        private Vector3 forward;
        public Vector3 Direction
        {
            get { return forward; }
            private set { forward = value; }
        }
        private Vector3 right;

        private float rotationX = 0;
        private float rotationY = 0;

        // szybkosci przesuwania sie i obrotu kamery.
        private float moveSpeed = 20.0f;
        private float rotateSpeed = 0.2f;

        // Zeby reagowac na ruchy myszki, musimy wiedziec jak bardzo uzytkownik przesunal kursor od poprzedniej klatki.
        // Aktualny stan kursora XNA pozwala odczytac, ale stan z poprzedniej klatki nalezy przechowywac samemu.
        //private Vector2 previousMousePos;

        // Kamere tworzy sie, podajac pozycje, punkt, w ktory kamera ma patrzec, oraz wektor "do gory" (prawie zawsze (0,1,0)).
        // Podaje sie ponadto czery paramtery potrzebne do utworzenia macierzy projekcji.
        public FpsCamera(Vector3 position, Vector3 targetPos, Vector3 upVector,
            float fieldOfViewRad, float aspectRatio, float nearPlaneDistance, float farPlaneDistance)
        {
            this.Position = position;
            this.target = targetPos;
            this.up = upVector;

            // Warto wiedziec, kiedy potrzebujemy wektorow znormalizowanych (o jednostkowej dlugosci)
            // i normalizowac je kiedy trzeba.
            up.Normalize();

            forward = target - Position;
            forward.Normalize();

            right = Vector3.Cross(forward, up);
            right.Normalize();

            // Tworzenie macierzy widoku. Jest wywolywane w metodzie Update, ale zanim sie ona wywola po raz pierwszy,
            // dobrze jest juz miec utworzona macierz View.
            UpdateViewMatrix();

            // Tworzenie macierzy projekcji.
            Projection = Matrix.CreatePerspectiveFieldOfView(fieldOfViewRad,
                aspectRatio, nearPlaneDistance, farPlaneDistance);

            Mouse.SetPosition(VideoSettings.Instance.ScreenWidth / 2, VideoSettings.Instance.ScreenHeight / 2);
        }

        public virtual void Update(float deltaTime)
        {
            // Modyfikacja pozycji przy uzyciu odpowiedniego wektora kierunku (znormalizowanego! to wazne!),
            // inputu uzytkownika, szybkosci ruchu kamery i ilosci czasu, ktory uplynal od poprzedniego Update.
            if (Keyboard.GetState().IsKeyDown(Keys.W))
            {
                Position += forward * moveSpeed * deltaTime;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.S))
            {
                Position -= forward * moveSpeed * deltaTime;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.A))
            {
                Position -= right * moveSpeed * deltaTime;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.D))
            {
                Position += right * moveSpeed * deltaTime;
            }
            if (Keyboard.GetState().IsKeyDown(Keys.Space))
            {
                Position += new Vector3(0.0f, moveSpeed * deltaTime, 0.0f);
            }
            if (Keyboard.GetState().IsKeyDown(Keys.LeftControl))
            {
                Position += new Vector3(0.0f, -moveSpeed * deltaTime, 0.0f);
            }
            if (Keyboard.GetState().IsKeyDown(Keys.LeftShift))
            {
                moveSpeed = 50;
            }
            else
            {
                moveSpeed = 20;
            }

            // Zapisanie stanu kursora.
            Vector2 mousePos = new Vector2(Mouse.GetState().X, Mouse.GetState().Y);

            /*
             * Uaktualnienie zmiennych reprezentujacych obrot kamery przy uzyciu wielkosci przesuniecia kursora
             * i szybkosci obroru.
             * Zauwaz, ze roznica w pozycji kursora na osi X ekranu wplywa na obrot na osi Y kamery i odwrotnie.
             */
            rotationX -= (mousePos.Y - VideoSettings.Instance.ScreenHeight / 2) * rotateSpeed;
            rotationY -= (mousePos.X - VideoSettings.Instance.ScreenWidth / 2) * rotateSpeed;

            Mouse.SetPosition(VideoSettings.Instance.ScreenWidth / 2, VideoSettings.Instance.ScreenHeight / 2);

            // Ograniczenie obrotu wokol osi X do 90 stopni
            // (Aby kamera nie "wywinela sie do gory nogami" gdy patrzysz maksymalnie w dol, lub maksymalnie do gory.)
            if (rotationX > 88.0f)
            {
                rotationX = 88.0f;
            }
            if (rotationX < -88.0f)
            {
                rotationX = -88.0f;
            }

            // Utworzenie macierzy obrotu wokol osi X i Y.
            Matrix xRotationMatrix = Matrix.CreateRotationX(MathHelper.ToRadians(rotationX));
            Matrix yRotationMatrix = Matrix.CreateRotationY(MathHelper.ToRadians(rotationY));

            /*
             * Utworzenie macierzy reprezentujacej zlozenie obu obrotow.
             * Najpierw nalezy zastosowac przeksztalcenie wokol osi Y, a potem X
             * (sprobuj to sobie wyobrazic lub narysowac, aby zrozumiec dlaczego).
             * UWAGA: W XNA jest przyjeta taka konwencja (zwiazane jest to z ukladem macierzy),
             * w ktorej przeksztalcenie, ktore nalezy zastosowac jako pierwsze, pojawia sie z prawej strony!
             * (Dotyczy to tylko skladania macierzy przeksztalcen w jedna macierz - jesli mnozymy wektor osobno przez kazda z macierzy,
             * robimy to w naturalnej kolejnosci.)
             */
            Matrix rotationMatrix = xRotationMatrix * yRotationMatrix;

            /*
             * Metoda TransformNormal odpowiada opisanemu na poprzednich zajeciach dodaniu do wektora czwartej wspolrzedniej
             * rownej 0 (bo kierunek, a nie 1 jak w przypadku punktow!) i pomnozeniu otrzymanego wektora 4-elementowego przez macierz.
             * (no i na koncu obcieciu wyniku z powrotem do trzech elementow).
             * Rownowaznie moznaby napisac:
             * 
             * Vector4 forward4 = new Vector4(Vector3.Forward, 0.0f);
             * forward4 = Vector4.Transform(forward4, rotationMatrix);
             * forward.X = forward4.X;
             * forward.Y = forward4.Y;
             * forward.Z = forward4.Z;
             * 
             * Zauwaz, ze poniewaz pola xRotationMatrix i yRotationMatrix skladuja CALKOWITY obrot obiektu
             * (a nie tylko niewielki obrot powstaly w aktualnej klatce), jako poczatkowe wektory ktore beda obracane,
             * bierzemy nie wektory forward i right z poprzedniej klatki, tylko "prawdziwe" forward i right, czyli (0,0,-1) i (1,0,0).
             * Moznaby zrobic to drugim sposobem - obracac wektory forward i right obliczone w poprzedniej klatce przy uzyciu macierzy
             * stworzonych z niewielkich obrotow powstalych w aktualnej klatce. Powoduje to jednak narastajaca kumulacje bledow
             * numerycznych - nasz sposob liczy caly obrot za kazdym razem od nowa (nie korzysta z poprzednich wynikow obliczen)
             * wiec bledy nie kumuluja sie.
             */
            forward = Vector3.TransformNormal(Vector3.Forward, rotationMatrix);
            right = Vector3.TransformNormal(Vector3.Right, rotationMatrix);



            // target, reprezentujacy punkt, w ktory ma patrzec kamery, musi lezec po dodatniej stronie osi patrzenia.
            // Ale gdzie dokladnie lezy na tej osi, to juz nie ma znaczenia. Najprosciej wziac aktualna pozycje
            // i dodac do niej wektor "do przodu" kamery, aby otrzymac punkt na tej osi.
            target = Position + forward;

            // Rekonstrukcja macierzy widoku.
            UpdateViewMatrix();
        }

        protected void UpdateViewMatrix()
        {
            View = Matrix.CreateLookAt(Position, target, up);
        }
    }
}
